package top.guoziyang.mydb.backend.dm;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.google.common.primitives.Bytes;

import top.guoziyang.mydb.backend.common.SubArray;
import top.guoziyang.mydb.backend.dm.dataItem.DataItem;
import top.guoziyang.mydb.backend.dm.logger.Logger;
import top.guoziyang.mydb.backend.dm.page.Page;
import top.guoziyang.mydb.backend.dm.page.PageX;
import top.guoziyang.mydb.backend.dm.pageCache.PageCache;
import top.guoziyang.mydb.backend.tm.TransactionManager;
import top.guoziyang.mydb.backend.utils.Panic;
import top.guoziyang.mydb.backend.utils.Parser;


/**
 * recover 例程主要是两步：重做所有已完成事务，撤销所有未完成事务
 * */
public class Recover {

    private static final byte LOG_TYPE_INSERT = 0; //插入日志
    private static final byte LOG_TYPE_UPDATE = 1; //删除日志

    private static final int REDO = 0; //redo日志
    private static final int UNDO = 1; //undo日志

    /**
     * 插入日志格式信息
     * */
    static class InsertLogInfo {
        long xid;
        int pgno;
        short offset;
        byte[] raw;
    }

    /**
     * 更新日志格式信息
     * */
    static class UpdateLogInfo {
        long xid;
        int pgno;
        short offset;
        byte[] oldRaw;
        byte[] newRaw;
    }

    /**
     * 恢复策略
     */
    public static void recover(TransactionManager tm, Logger lg, PageCache pc) {
        System.out.println("Recovering...");

        lg.rewind();
        int maxPgno = 0;
        // 遍历日志记录，计算最大页数
        while(true) {
            byte[] log = lg.next();
            if(log == null) break;
            int pgno;
            if(isInsertLog(log)) {
                InsertLogInfo li = parseInsertLog(log);
                pgno = li.pgno;
            } else {
                UpdateLogInfo li = parseUpdateLog(log);
                pgno = li.pgno;
            }
            if(pgno > maxPgno) {
                maxPgno = pgno;
            }
        }
        // 如果maxPgno为0，说明没有数据页，设置为1，因为MyDB除了普通数据页外还有一个“第一页”
        if(maxPgno == 0) {
            maxPgno = 1;
        }
        // 截断文件到指定的页面大小，删除因崩溃导致未完全写入的页面
        pc.truncateByPgno(maxPgno);
        System.out.println("Truncate to " + maxPgno + " pages.");

        redoTranscations(tm, lg, pc);
        System.out.println("Redo Transactions Over.");

        undoTranscations(tm, lg, pc);
        System.out.println("Undo Transactions Over.");

        System.out.println("Recovery Over.");
    }

    /**
     *  重做事务
     */
    private static void redoTranscations(TransactionManager tm, Logger lg, PageCache pc) {
        lg.rewind();
        // 循环读取每一条完整的日志
        while(true) {
            byte[] log = lg.next();
            if(log == null) break;
            if(isInsertLog(log)) {
                // 如果是插入操作
                InsertLogInfo li = parseInsertLog(log);
                long xid = li.xid;
                // 如果事务是非活跃状态(已提交),重做事务;否则什么也不做
                if(!tm.isActive(xid)) {
                    doInsertLog(pc, log, REDO);
                }
            } else {
                // 如果是更新操作
                UpdateLogInfo xi = parseUpdateLog(log);
                long xid = xi.xid;
                if(!tm.isActive(xid)) {
                    doUpdateLog(pc, log, REDO);
                }
            }
        }
    }

    /**
     *  回滚事务
     */
    private static void undoTranscations(TransactionManager tm, Logger lg, PageCache pc) {
        Map<Long, List<byte[]>> logCache = new HashMap<>();
        lg.rewind();
        // 循环读取每一条完整的日志
        while(true) {
            byte[] log = lg.next();
            if(log == null) break;
            if(isInsertLog(log)) {
                InsertLogInfo li = parseInsertLog(log);
                long xid = li.xid;
                //如果事务是活跃状态(未提交),回滚事务;否则什么也不做
                if(tm.isActive(xid)) {
                    if(!logCache.containsKey(xid)) {
                        logCache.put(xid, new ArrayList<>());
                    }
                    logCache.get(xid).add(log);
                }
            } else {
                UpdateLogInfo xi = parseUpdateLog(log);
                long xid = xi.xid;
                if(tm.isActive(xid)) {
                    if(!logCache.containsKey(xid)) {
                        logCache.put(xid, new ArrayList<>());
                    }
                    logCache.get(xid).add(log);
                }
            }
        }

        // 对所有active log进行倒序undo
        for(Entry<Long, List<byte[]>> entry : logCache.entrySet()) {
            List<byte[]> logs = entry.getValue();
            for (int i = logs.size()-1; i >= 0; i --) {
                byte[] log = logs.get(i);
                if(isInsertLog(log)) {
                    doInsertLog(pc, log, UNDO);
                } else {
                    doUpdateLog(pc, log, UNDO);
                }
            }
            // 设置事务的状态为：已回滚
            tm.abort(entry.getKey());
        }
    }

    /**
     *  判断是否是插入日志
     */
    private static boolean isInsertLog(byte[] log) {
        return log[0] == LOG_TYPE_INSERT;
    }

    /**
     * [LogType] [XID] [UID] [OldRaw] [NewRaw]
     * LogType: 1字节
     * XID: 8字节
     * UID: 8字节
     */
    private static final int OF_TYPE = 0;  // 日志类型的偏移量
    private static final int OF_XID = OF_TYPE+1; // 事务id的偏移量
    private static final int OF_UPDATE_UID = OF_XID+8; //数据唯一id的偏移量
    private static final int OF_UPDATE_RAW = OF_UPDATE_UID+8; //旧、新数据内容的偏移量

    /**
     * 生成一个“更新日志”（updateLog）的字节数组，记录某次更新操作的相关信息
     */
    public static byte[] updateLog(long xid, DataItem di) {
        byte[] logType = {LOG_TYPE_UPDATE};
        byte[] xidRaw = Parser.long2Byte(xid);
        byte[] uidRaw = Parser.long2Byte(di.getUid());
        byte[] oldRaw = di.getOldRaw(); // 更新前的数据快照，用于回滚操作
        SubArray raw = di.getRaw();
        byte[] newRaw = Arrays.copyOfRange(raw.raw, raw.start, raw.end); // 更新后的数据快照
        return Bytes.concat(logType, xidRaw, uidRaw, oldRaw, newRaw);
    }

    /**
     * 解析一条更新日志,将日志的字节读写log转换成UpdateLogInfo对象
     */
    private static UpdateLogInfo parseUpdateLog(byte[] log) {
        UpdateLogInfo li = new UpdateLogInfo();
        li.xid = Parser.parseLong(Arrays.copyOfRange(log, OF_XID, OF_UPDATE_UID));
        long uid = Parser.parseLong(Arrays.copyOfRange(log, OF_UPDATE_UID, OF_UPDATE_RAW));
        // 将uid的低16位(2字节)提取为offset，表示更新数据在页面中的偏移量
        li.offset = (short)(uid & ((1L << 16) - 1));
        // 截取uid的高32位，转换为int类型，得到的值是页面号pgno
        uid >>>= 32;
        li.pgno = (int)(uid & ((1L << 32) - 1));
        // 更新数据的总长度/2，得到oldRaw和newRaw的字节长度
        int length = (log.length - OF_UPDATE_RAW) / 2;
        // 读取oldRaw和newRaw
        li.oldRaw = Arrays.copyOfRange(log, OF_UPDATE_RAW, OF_UPDATE_RAW+length);
        li.newRaw = Arrays.copyOfRange(log, OF_UPDATE_RAW+length, OF_UPDATE_RAW+length*2);
        return li;
    }

    /**
     * undo和redo对更新数据的重做和撤销操作
     */
    private static void doUpdateLog(PageCache pc, byte[] log, int flag) {
        int pgno;
        short offset;
        byte[] raw;
        if(flag == REDO) {
            // 如果是redo操作，取出新值，把数据更新到最新的姿态
            UpdateLogInfo xi = parseUpdateLog(log);
            pgno = xi.pgno;
            offset = xi.offset;
            raw = xi.newRaw;
        } else {
            // 如果是undo操作，取出旧值，把数据更新到事务开始之前的状态
            UpdateLogInfo xi = parseUpdateLog(log);
            pgno = xi.pgno;
            offset = xi.offset;
            raw = xi.oldRaw;
        }
        Page pg = null;
        try {
            pg = pc.getPage(pgno);
        } catch (Exception e) {
            Panic.panic(e);
        }
        try {
            PageX.recoverUpdate(pg, raw, offset);
        } finally {
            pg.release();
        }
    }


    /**
     * [LogType] [XID] [Pgno] [Offset] [Raw]
     * LogType: 1字节
     * XID: 8字节
     * Pgno: 4字节
     * Offset: 2字节
     */
    private static final int OF_INSERT_PGNO = OF_XID+8; //插入的目标页面编号的偏移量
    private static final int OF_INSERT_OFFSET = OF_INSERT_PGNO+4; //页面中插入位置的偏移量
    private static final int OF_INSERT_RAW = OF_INSERT_OFFSET+2; // 插入的数据的偏移量

    /**
     * 生成一个“插入日志”（InsertLog）的字节数组，记录某次插入操作的相关信息
     */
    public static byte[] insertLog(long xid, Page pg, byte[] raw) {
        byte[] logTypeRaw = {LOG_TYPE_INSERT};
        byte[] xidRaw = Parser.long2Byte(xid);
        byte[] pgnoRaw = Parser.int2Byte(pg.getPageNumber()); //数据插入到了哪一页，该页是普通数据页PageX
        byte[] offsetRaw = Parser.short2Byte(PageX.getFSO(pg)); //(PageX.getFSO(pg)获取该页空闲位置的偏移量,即新数据的插入位置
        return Bytes.concat(logTypeRaw, xidRaw, pgnoRaw, offsetRaw, raw); //拼接成InsertLog格式的字节数组
    }

    /**
     * 解析一条插入日志,将日志的字节读写log转换成InsertLogInfo对象
     */
    private static InsertLogInfo parseInsertLog(byte[] log) {
        // 创建一个InsertLogInfo类型的实例li，用于存储解析后的插入日志信息
        InsertLogInfo li = new InsertLogInfo();
        li.xid = Parser.parseLong(Arrays.copyOfRange(log, OF_XID, OF_INSERT_PGNO));
        li.pgno = Parser.parseInt(Arrays.copyOfRange(log, OF_INSERT_PGNO, OF_INSERT_OFFSET));
        li.offset = Parser.parseShort(Arrays.copyOfRange(log, OF_INSERT_OFFSET, OF_INSERT_RAW));
        li.raw = Arrays.copyOfRange(log, OF_INSERT_RAW, log.length);
        return li;
    }

    /**
     * undo和redo对插入数据的重做和撤销操作
     */
    private static void doInsertLog(PageCache pc, byte[] log, int flag) {
        InsertLogInfo li = parseInsertLog(log);
        Page pg = null;
        try {
            // 从缓存中获取这页数据
            pg = pc.getPage(li.pgno);
        } catch(Exception e) {
            Panic.panic(e);
        }
        /**
         * 这里无论flag是undo还是redo，都会执行插入操作，但插入的数据会根据flag的不同而不同。
         * 如果是undo操作，将li.raw数据标记为无效该操作标记了数据，使得数据被视为逻辑上的已删除，而不是物理删除数据
         * flag=redo时，将直接使用日志中的原始数据li.raw,意味着PageX.recoverInsert会插入该数据，恢复插入操作的结果
         */
        try {
            if(flag == UNDO) {
                DataItem.setDataItemRawInvalid(li.raw);
            }
            PageX.recoverInsert(pg, li.raw, li.offset);
        } finally {
            // 操作完成后释放数据页
            pg.release();
        }
    }
}
